﻿
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;

namespace WSU.ChekaParallel.TestTask
{
    static class TestCancel
    {
        // ************************************************************************* //
        private static void TestSimpleUsage()
        {
            CancellationTokenSource cancelSource = new CancellationTokenSource();
            CancellationToken cancelToken = cancelSource.Token;

            // chekanote: pay attention here, I use a action without any argument, so the token used in the action body
            // is not passed by the caller, but a captured variable
            Task atask = Task.Factory.StartNew(() =>
                                                   {
                                                       while (true)
                                                       {
                                                           // here "cancelToken" is a captured variable
                                                           cancelToken.ThrowIfCancellationRequested();

                                                           Console.Write(".");
                                                           Thread.Sleep(200);
                                                           Console.Write("-");
                                                       }// while
                                                   }, cancelToken);
            Console.WriteLine("Press any key to cancel the running task, ......");
            Console.ReadLine();

            cancelSource.Cancel();

            // ---------------------- check the canceled status
            try
            {
                atask.Wait();
                Debug.Fail("wait must throw exception when canceled.");
            }
            catch (AggregateException aexp)
            {
                Debug.Assert(aexp.InnerException is OperationCanceledException);
                Console.WriteLine("when canceled, it will trigger 'OperationCanceledException'");
            }

            Debug.Assert(atask.Status == TaskStatus.Canceled);
            Debug.Assert(atask.IsCanceled);
            Debug.Assert(atask.IsCompleted);
            Debug.Assert(atask.IsFaulted == false);
            Console.WriteLine("current status={0}", atask.Status);
        }

        /// <summary>
        /// from this sample, we can see that if we want the task's status is 'Canceled' other than 'Faulted'
        /// we must pass in the same CancelToken when constructing that task
        /// otherwise, when cts is canceled, the task is 'faulted', other than 'canceled'
        /// </summary>
        /// <param name="passingToken"></param>
        private static void TestPassingToken(bool passingToken)
        {
            CancellationTokenSource cts = new CancellationTokenSource();

            Action body = () =>
                              {
                                  cts.Token.WaitHandle.WaitOne();
                                  cts.Token.ThrowIfCancellationRequested();
                              };
            Task task = passingToken ? Task.Factory.StartNew(body, cts.Token) : Task.Factory.StartNew(body);

            Helper.Pause();
            cts.Cancel();

            try
            {
                task.Wait();
                Console.WriteLine("task successfully completes.");
            }
            catch (AggregateException aggregatedError)
            {
                aggregatedError.Flatten().Handle(ex => ex is OperationCanceledException);
                Console.WriteLine("Token Passed In='{0}',Task's Status='{1}'", passingToken, task.Status);
            }
        }

        // ************************************************************************* //
        #region [ detect cancel by polling ]

        // chekanote: when "postCancelAction" is invoked, the task is already in "Canceled" state
        private static void PollIsCancelled(Action<object> cancelableAction, Action postCancelAction = null)
        {
            CancellationTokenSource cancelTokenSrc = new CancellationTokenSource();

            CancellationToken cancelToken = cancelTokenSrc.Token;
            if (postCancelAction != null)
                cancelToken.Register(postCancelAction);

            Task cancelTask = Task.Factory.StartNew(cancelableAction, cancelToken, cancelToken);

            Console.WriteLine("Press any key to cancel the task");
            Console.ReadLine();

            Console.WriteLine("task is canceling, ... ...");
            cancelTokenSrc.Cancel();

            try
            {
                cancelTask.Wait();
            }
            catch (AggregateException exp)
            {
                Debug.Assert(exp.InnerException is OperationCanceledException);
            }

            Console.WriteLine("task has been cancelled, task status is <{0}>", cancelTask.Status);
            Debug.Assert(TaskStatus.Canceled == cancelTask.Status);
            Debug.Assert(cancelTask.IsCanceled);
        }

        private static void TestManualPollCancelled()
        {
            PollIsCancelled((obj) =>
                {
                    CancellationToken canceltoken = (CancellationToken)obj;
                    while (true)
                    {
                        if (canceltoken.IsCancellationRequested)
                        {
                            Console.WriteLine("!!! Cancel is detected, clearing up resources now");
                            Thread.Sleep(1000);
                            Console.WriteLine("!!! throw exception to cancel current task");

                            // chekanote: it is important to pass the "CancelToken" as the parameter in the constructor 
                            // of the exception when throwing CancelledException
                            // if not passing that token, though this task can still be cancelled, but its status
                            // will not be marked as "Canceled", instead, will be marked as "Faulted"
                            // then in some cases, especially when there are following tasks continued "OnlyCanceled", then those
                            // continuations will never be invoked
                            throw new OperationCanceledException(canceltoken);
                            // throw new OperationCanceledException();
                        }
                        else
                        {
                            Console.Write(".");
                            Thread.Sleep(200);
                            Console.Write("-");
                        }
                    }
                });
        }

        private static void WaitforCancel(object obj)
        {
            CancellationToken canceltoken = (CancellationToken)obj;
            while (true)
            {
                // chekanote: use "ThrowIfCancellationRequested" leave no chance to clean up resources
                canceltoken.ThrowIfCancellationRequested();

                Console.Write(".");
                Thread.Sleep(200);
                Console.Write("-");
            }
        }

        private static void TestPollCanceledByException()
        {
            PollIsCancelled(WaitforCancel);
        }

        #endregion

        // ************************************************************************* //
        #region [ test canceled delegate ]

        private static void TestCanceledCallback()
        {
            PollIsCancelled(WaitforCancel, () =>
                {
                    Console.WriteLine("Canceled callback has been invoked");
                });
        }

        #endregion

        // ************************************************************************* //
        #region [ test wait on cancel token ]

        private static void TestWaitOnCancelToken()
        {
            CancellationTokenSource cancelTokenSource = new CancellationTokenSource();
            CancellationToken cancelToken = cancelTokenSource.Token;

            Task cancelabletask = Task.Factory.StartNew(WaitforCancel, cancelToken, cancelToken);

            Console.WriteLine("Press any key to cancel the task, ......");
            Console.ReadLine();

            cancelTokenSource.Cancel();

            // ---------- instead wait on task, here wait on the cancel token
            cancelToken.WaitHandle.WaitOne();

            // chekanote: when "WaitOne" returns, it is only indicates "Cancel" method is invoked on CancellationTokenSource
            // but there is no guarantation that task itself is caneled, task may be still running
            Console.WriteLine("'Canel' is invoked, but not canceled yet,current status is <{0}>", cancelabletask.Status);

            // ---------- wait for real termination
            try
            {
                cancelabletask.Wait();
            }
            catch (AggregateException aggexp)
            {
                Debug.Assert(aggexp.InnerException is OperationCanceledException);
            }
            Debug.Assert(cancelabletask.IsCanceled);
            Console.WriteLine("now, task is actually canceled, status={0}", cancelabletask.Status);
            Debug.Assert(cancelabletask.Status == TaskStatus.Canceled);
        }

        #endregion

        // ************************************************************************* //
        #region [ cancel multiple task ]

        private static void HelperForMultipleCancel(object obj)
        {
            Tuple<CancellationToken, string> state = (Tuple<CancellationToken, string>)obj;

            while (true)
            {
                state.Item1.ThrowIfCancellationRequested();

                Console.WriteLine(state.Item2);
                Thread.Sleep(250);
            }
        }

        public static void TestMultipleCancel()
        {
            CancellationTokenSource cancelSource = new CancellationTokenSource();
            CancellationToken cancelToken = cancelSource.Token;

            Task[] tasks = new Task[2];
            // chekanote: if missing the last parameter, that not passing the token
            // then when "cancel" is invoked, tasks will still be canceled, but status of terminated tasks
            // will not be "Canceled", but "Faulted", and "IsCanceled" property returns false
            tasks[0] = Task.Factory.StartNew(HelperForMultipleCancel, Tuple.Create(cancelToken, "task-1"), cancelToken);
            tasks[1] = Task.Factory.StartNew(HelperForMultipleCancel, Tuple.Create(cancelToken, "task-2"), cancelToken);

            Console.WriteLine("Press any key to cancel multiple tasks at all");
            Console.ReadLine();
            cancelSource.Cancel();

            try
            {
                Task.WaitAll(tasks);
            }
            catch (AggregateException aggExp)
            {
                foreach (Exception innerexp in aggExp.InnerExceptions)
                {
                    Debug.Assert(innerexp is OperationCanceledException);
                }
            }

            foreach (Task atask in tasks)
            {
                Debug.Assert(atask.IsCanceled);
                Console.WriteLine("{0} canceled,status={1}",
                    ((Tuple<CancellationToken, string>)atask.AsyncState).Item2,
                    atask.Status);
            }
            Console.WriteLine("all tasks have been canceled.");
        }

        #endregion

        // ************************************************************************* //
        #region [ test detect any cancellation ]

        private static Task StartCancelableTask(CancellationToken cancelToken)
        {
            return Task.Factory.StartNew(WaitforCancel, cancelToken, cancelToken);
        }

        private static void WaitTaskCanceled(Task atask)
        {
            try
            {
                atask.Wait();
            }
            catch (AggregateException aggExp)
            {
                Debug.Assert(aggExp.InnerException is OperationCanceledException);
            }
            Debug.Assert(atask.IsCanceled);
            Console.WriteLine("atask canceled with status={0}", atask.Status.ToString());
        }

        /// <summary>
        /// token of composite cancellation source will cancel the running task
        /// when any of the raw cancellation source are canceled
        /// </summary>
        private static void TestCompositeCancelSource()
        {
            // ---------------- create original, raw cancellation source
            CancellationTokenSource cancelSource1 = new CancellationTokenSource();
            CancellationTokenSource cancelSource2 = new CancellationTokenSource();

            // ---------------- create a composite cancellation token source
            CancellationTokenSource compositeCancelSource = CancellationTokenSource.CreateLinkedTokenSource(
                cancelSource1.Token, cancelSource2.Token);

            // ---------------- start task
            Task cancelTask = StartCancelableTask(compositeCancelSource.Token);

            // ---------------- invoke cancellation by any of the raw, original cancellation source
            Console.WriteLine("Press any key to canel the task, ......");
            Console.ReadLine();

            cancelSource2.Cancel();

            // ---------------- wait task truely canceled
            WaitTaskCanceled(cancelTask);
        }

        #endregion

        // ************************************************************************* //
        #region [ test entry ]

        public static void TestMain()
        {
            // TestSimpleUsage();
            TestPassingToken(true);
            TestPassingToken(false);
            // TestManualPollCancelled();
            // TestPollCanceledByException();
            // TestCanceledCallback();
            // TestWaitOnCancelToken();
            // TestMultipleCancel();
            // TestCompositeCancelSource();
        }

        #endregion
    }
}